1#!/usr/bin/python
  2# -*- coding:utf-8 -*-
  3#
  4# Python driver for GDEM035T81 3.5" e-Paper display
  5# Controller: SSD1685
  6# Resolution: 384x184
  7#
  8# Based on GxEPD2 driver by Jean-Marc Zingg:
  9# https://github.com/ZinggJM/GxEPD2/blob/master/src/gdey/GxEPD2_290_GDEY029T71H.cpp
 10# Adapted for Waveshare HAT on Raspberry Pi (spidev + RPi.GPIO)
 11
 12import RPi.GPIO as GPIO
 13import spidev
 14import time
 15
 16# Pin definitions (Waveshare HAT standard pinout)
 17RST_PIN  = 17
 18DC_PIN   = 25
 19CS_PIN   = 8
 20BUSY_PIN = 24
 21
 22# Display resolution
 23WIDTH  = 184
 24HEIGHT = 384
 25
 26# SSD1685 source shift (OTP set for wider panel; shift to align to actual TFT)
 27SOURCE_SHIFT = 8  # adjust if image is offset horizontally
 28
 29
 30class EPD:
 31    def __init__(self):
 32        self.reset_pin = RST_PIN
 33        self.dc_pin    = DC_PIN
 34        self.cs_pin    = CS_PIN
 35        self.busy_pin  = BUSY_PIN
 36        self.width     = WIDTH
 37        self.height    = HEIGHT
 38        self._init_done = False
 39
 40        GPIO.setmode(GPIO.BCM)
 41        GPIO.setwarnings(False)
 42        GPIO.setup(self.reset_pin, GPIO.OUT)
 43        GPIO.setup(self.dc_pin,    GPIO.OUT)
 44        GPIO.setup(self.cs_pin,    GPIO.OUT)
 45        GPIO.setup(self.busy_pin,  GPIO.IN)
 46
 47        self.spi = spidev.SpiDev()
 48        self.spi.open(0, 0)
 49        self.spi.max_speed_hz = 4000000
 50        self.spi.mode = 0b00
 51
 52    # ------------------------------------------------------------------ low level
 53
 54    def _reset(self):
 55        GPIO.output(self.reset_pin, GPIO.HIGH)
 56        time.sleep(0.2)
 57        GPIO.output(self.reset_pin, GPIO.LOW)
 58        time.sleep(0.002)
 59        GPIO.output(self.reset_pin, GPIO.HIGH)
 60        time.sleep(0.2)
 61
 62    def _send_command(self, cmd):
 63        GPIO.output(self.dc_pin, GPIO.LOW)
 64        GPIO.output(self.cs_pin, GPIO.LOW)
 65        self.spi.writebytes([cmd])
 66        GPIO.output(self.cs_pin, GPIO.HIGH)
 67
 68    def _send_data(self, data):
 69        GPIO.output(self.dc_pin, GPIO.HIGH)
 70        GPIO.output(self.cs_pin, GPIO.LOW)
 71        if isinstance(data, int):
 72            self.spi.writebytes([data])
 73        else:
 74            # send in chunks to avoid SPI buffer limits
 75            for i in range(0, len(data), 4096):
 76                self.spi.writebytes(data[i:i+4096])
 77        GPIO.output(self.cs_pin, GPIO.HIGH)
 78
 79    def _wait_busy(self, timeout_ms=5000):
 80        """BUSY pin: LOW = busy, HIGH = idle (SSD1685)"""
 81        deadline = time.time() + timeout_ms / 1000.0
 82        while GPIO.input(self.busy_pin) == GPIO.LOW:
 83            if time.time() > deadline:
 84                print("WARNING: busy timeout")
 85                break
 86            time.sleep(0.01)
 87
 88    # ------------------------------------------------------------------ RAM area
 89
 90    def _set_ram_area(self, x, y, w, h):
 91        x += SOURCE_SHIFT
 92        # x increase, y increase (normal scan direction)
 93        self._send_command(0x11)
 94        self._send_data(0x03)
 95        # X start / end (byte addresses)
 96        self._send_command(0x44)
 97        self._send_data(x // 8)
 98        self._send_data((x + w) // 8 - 1)
 99        # Y start / end (two bytes each, LSB first)
100        self._send_command(0x45)
101        self._send_data(y % 256)
102        self._send_data(y // 256)
103        self._send_data((y + h - 1) % 256)
104        self._send_data((y + h - 1) // 256)
105        # Set RAM x/y counters
106        self._send_command(0x4E)
107        self._send_data(x // 8)
108        self._send_command(0x4F)
109        self._send_data(y % 256)
110        self._send_data(y // 256)
111
112    # ------------------------------------------------------------------ init
113
114    def init(self):
115        self._reset()
116        time.sleep(0.01)
117
118        self._send_command(0x12)  # SWRESET
119        time.sleep(0.01)
120
121        # Driver output control
122        self._send_command(0x01)
123        self._send_data((self.height - 1) % 256)
124        self._send_data((self.height - 1) // 256)
125        self._send_data(0x00)
126
127        # Border waveform
128        self._send_command(0x3C)
129        self._send_data(0x05)
130
131        # Use built-in temperature sensor
132        self._send_command(0x18)
133        self._send_data(0x80)
134
135        # Display update control (normal, no bypass)
136        self._send_command(0x21)
137        self._send_data(0x00)
138        self._send_data(0x00)
139
140        self._set_ram_area(0, 0, self.width, self.height)
141        self._init_done = True
142
143    # ------------------------------------------------------------------ clear
144
145    def clear(self, color=0xFF):
146        """Clear screen. color=0xFF = white, 0x00 = black."""
147        if not self._init_done:
148            self.init()
149        buf = [color] * (self.width * self.height // 8)
150        # write to both current (0x24) and previous (0x26) buffers
151        for cmd in (0x26, 0x24):
152            self._set_ram_area(0, 0, self.width, self.height)
153            self._send_command(cmd)
154            self._send_data(buf)
155        self._refresh_full()
156
157    # ------------------------------------------------------------------ display
158
159    def getbuffer(self, image):
160        buf = [0xFF] * (self.width * self.height // 8)
161
162        image = image.convert('1')
163        image = image.rotate(90, expand=True)
164        image = image.resize((self.width, self.height))
165
166        pixels = image.load()
167
168        stride = (self.width + 7) // 8  # <-- FIX
169
170        for y in range(self.height):
171            for x in range(self.width):
172                if pixels[x, y] == 0:
173                    index = (x // 8) + y * stride
174                    buf[index] &= ~(0x80 >> (x % 8))
175
176        return buf
177
178    def display(self, buf):
179        """Send buffer and do a full refresh."""
180        if not self._init_done:
181            self.init()
182        for cmd in (0x26, 0x24):  # <-- IMPORTANT
183       	    self._set_ram_area(0, 0, self.width, self.height)
184            self._send_command(cmd)
185            self._send_data(buf)
186        self._refresh_full()
187
188    def display_partial(self, buf):
189        """Send buffer and do a fast partial refresh."""
190        if not self._init_done:
191            self.init()
192        self._set_ram_area(0, 0, self.width, self.height)
193        self._send_command(0x24)
194        self._send_data(buf)
195        self._refresh_partial()
196
197    # ------------------------------------------------------------------ refresh
198
199    def _refresh_full(self):
200        # bypass RED channel, use fast full update with temperature
201        self._send_command(0x21)
202        self._send_data(0x40)  # bypass RED as 0
203        self._send_data(0x00)
204        # write temperature register (110 dec = 0x6E = ~110°C forces fast LUT)
205        self._send_command(0x1A)
206        self._send_data(0x6E)
207        self._send_data(0x00)
208        # load temperature value
209        self._send_command(0x22)
210        self._send_data(0x91)
211        self._send_command(0x20)
212        time.sleep(0.002)
213        # full update sequence
214        self._send_command(0x22)
215        self._send_data(0xC7)
216        self._send_command(0x20)
217        self._wait_busy(5000)
218
219    def _refresh_partial(self):
220        self._send_command(0x21)
221        self._send_data(0x00)
222        self._send_data(0x00)
223        self._send_command(0x22)
224        self._send_data(0xDC)
225        self._send_command(0x20)
226        self._wait_busy(1000)
227
228    # ------------------------------------------------------------------ power
229
230    def sleep(self):
231        self._send_command(0x10)  # deep sleep
232        self._send_data(0x01)
233        time.sleep(2)
234
235    def close(self):
236        self.spi.close()
237        GPIO.cleanup()
238
239
240
241if __name__ == '__main__':
242    import logging
243    logging.basicConfig(level=logging.INFO)
244    from PIL import Image
245
246    epd = EPD()
247    print("init...")
248    epd.init()
249    print("clear...")
250    epd.clear()
251    print("loading image...")
252    img = Image.open('test.png')
253    print("displaying...")
254    epd.display(epd.getbuffer(img))
255    print("sleep...")
256    epd.sleep()
257    epd.close()
258    print("done!")